import datetime
import json
import uuid

from flask import Markup
from flask_appbuilder import Model
from sqlalchemy import BigInteger, Column, DateTime, ForeignKey, Integer, String, Text
from sqlalchemy.orm import relationship

from myapp.config import SERVICE_NAMESPACE
from myapp.const.service import ServiceStatus
from myapp.models.base import MyappModelBase
from myapp.models.helpers import AuditMixinNullable, parse_model_path
from myapp.models.model_model_version import Model_Version
from myapp.models.model_team import Project
from myapp.models.model_train_model import Training_Model
from myapp.utils.env import is_modelarts
from myapp.utils.exception import log_exception


metadata = Model.metadata


class service_common:
    def get_node_selector(self):
        return self.get_default_node_selector(
            self.project.node_selector, self.resource_gpu, 'service'
        )

    @property
    def namespace(self):
        namespace = SERVICE_NAMESPACE
        if self.created_by_fk is None:
            raise Exception('created_by_fk is None')

        return f'{namespace}-{self.created_by_fk}'


class Service(Model, AuditMixinNullable, MyappModelBase, service_common):
    __tablename__ = 'service'
    id = Column(Integer, primary_key=True)
    project_id = Column(Integer, ForeignKey('project.id'))
    project = relationship(Project, foreign_keys=[project_id])

    name = Column(String(100), nullable=False, unique=True)  # Used to generate pod service and vs
    label = Column(String(100), nullable=False)
    images = Column(String(200), nullable=False)
    working_dir = Column(String(100), default='')
    command = Column(String(1000), default='')
    args = Column(Text, default='')
    env = Column(Text, default='')
    volume_mount = Column(String(200), default='')
    node_selector = Column(String(100), default='service=true')
    replicas = Column(Integer, default=1)
    ports = Column(String(100), default='80')
    resource_memory = Column(String(100), default='2G')
    resource_cpu = Column(String(100), default='2')
    resource_gpu = Column(String(100), default='0')
    deploy_time = Column(String(100), nullable=False, default=datetime.datetime.now)
    host = Column(String(200), default='')
    expand = Column(Text(65536), default='{}')

    describe = Column(String(1000), default='', comment='服务描述')
    model_id = Column(Integer, ForeignKey('model.id'), comment='模型ID')
    model = relationship(Training_Model, foreign_keys=[model_id])
    status = Column(
        String(200), default='stopped', comment='服务状态'
    )  # 服务状态 running/deploying/concerning/failed/stopped/finished
    error_msg = Column(Text(65536), default='', comment='错误信息')  # 错误信息
    configs = Column(
        Text(65536), default='{}', comment='服务资源配额、分流配置'
    )  # 服务资源配额、分流
    ratelimit = Column(
        String(1000), default='{}', server_default='{}', comment='服务限流配置'
    )  # 服务限流配置
    ma_id = Column(String(200), default='', comment='modelarts的服务ID')  # modelarts的服务ID

    invocation_times = Column(
        BigInteger, nullable=False, server_default='0', default=0
    )  # 调用成功的次数
    failed_times = Column(
        BigInteger, nullable=False, server_default='0', default=0
    )  # 调用失败次数
    deploy_history = Column(Text(65536), default='{}', comment='部署历史')  # 部署历史
    advanced = Column(Text(65536), default='{}', comment='高级配置')  # 高级配置
    running_start_time = Column(DateTime, nullable=True, comment='运行开始时间')
    acc_running_time = Column(Integer, server_default='0', default=0, comment='运行时长')
    region = Column(String(100), nullable=False, default='default', server_default='default', comment='地区')

    @property
    def health(self):
        service_advanced = self.advanced
        service_advanced = json.loads(service_advanced) if service_advanced else {}
        if service_advanced is None:
            service_advanced = {}
        health = service_advanced.get('health')
        return health

    # 部署开始时间
    @property
    def deploy_start_time(self):
        start_time = self.deploy_time
        if start_time is None or start_time == '':
            return None
        if ',' in start_time:
            start_time = start_time.split(',')[0]

        start_time = datetime.datetime.strptime(start_time, '%Y-%m-%d %H:%M:%S.%f')
        return start_time

    # 部署完成时间
    @property
    def deploy_completed_time(self):
        completed_time = self.deploy_time
        if completed_time is None or completed_time == '':
            return None
        if ',' in completed_time:
            completed_time = completed_time.split(',')[1]

        completed_time = datetime.datetime.strptime(completed_time, '%Y-%m-%d %H:%M:%S.%f')
        return completed_time

    @property
    def deploy_timeout(self):
        service_advanced = self.advanced
        service_advanced = json.loads(service_advanced) if service_advanced else {}
        if service_advanced is None:
            service_advanced = {}
        deploy_timeout = service_advanced.get('deploy_timeout', 60 * 60)
        return deploy_timeout

    # 服务自动停止（秒）
    @property
    def auto_stop(self):
        service_advanced = self.advanced
        service_advanced = json.loads(service_advanced) if service_advanced else {}
        if service_advanced is None:
            service_advanced = {}
        auto_stop = service_advanced.get('auto_stop', {'enable': False})
        if 'enable' not in auto_stop:
            auto_stop['enable'] = False

        return auto_stop

    @property
    def clear(self):
        return Markup(f'<a href="/service_modelview/clear/{self.id}">清理</a>')

    def __repr__(self):
        return self.name

    def get_acc_running_time(self):
        if self.status != ServiceStatus.running.value:
            return self.acc_running_time

        if self.running_start_time is None:
            return self.acc_running_time

        acc_running_time = self.acc_running_time
        time_diff = datetime.datetime.now() - self.running_start_time
        if acc_running_time is None:
            acc_running_time = 0
        if time_diff.total_seconds() > 0:
            return acc_running_time + time_diff.total_seconds()

        return acc_running_time

    @property
    def remaining_run_time(self):
        # 检查是否开启自动停止
        auto_stop = self.auto_stop
        if auto_stop['enable'] is False:
            return None

        if self.status != ServiceStatus.running.value:
            return 0

        if self.running_start_time is None:
            return auto_stop['seconds']

        running_time = datetime.datetime.now() - self.running_start_time
        running_time_seconds = running_time.seconds
        if running_time_seconds < 0:
            running_time_seconds = 0

        left_time = auto_stop['seconds'] - running_time_seconds
        if left_time < 1:
            left_time = 1
        return left_time

    @property
    def total_invocation_times(self):
        try:
            success_times = int(self.invocation_times)
        except Exception:
            success_times = 0

        try:
            failed_times = int(self.failed_times)
        except Exception:
            failed_times = 0
        return failed_times + success_times


class InferenceService(Model, AuditMixinNullable, MyappModelBase, service_common):
    __tablename__ = 'inferenceservice'
    id = Column(Integer, primary_key=True)
    project_id = Column(Integer, ForeignKey('project.id'))
    project = relationship(Project, foreign_keys=[project_id])

    name = Column(String(100), nullable=True)
    label = Column(String(100), nullable=False)

    service_type = Column(String(100), nullable=True, default='serving')
    model_name = Column(String(200), default='')
    model_version = Column(String(200), default='')
    model_path = Column(String(200), default='')
    model_type = Column(String(200), default='')
    model_input = Column(Text(65536), default='')
    model_output = Column(Text(65536), default='')
    pool_type = Column(String(200), default='private')  # 资源池类型
    inference_config = Column(Text(65536), default='')  # make configmap
    model_status = Column(String(200), default='offline')
    status = Column(
        String(200), default='stopped'
    )  # 服务状态 running/deploying/concerning/failed/stopped/finished
    error_msg = Column(Text(65536), default='')  # 错误信息

    transformer = Column(String(200), default='')  # pre process and post process

    images = Column(String(200), nullable=False)
    working_dir = Column(String(100), default='')
    command = Column(String(1000), default='')
    args = Column(Text, default='')
    env = Column(Text, default='')
    volume_mount = Column(String(2000), default='')
    node_selector = Column(String(100), default='service=true')
    min_replicas = Column(Integer, default=1)
    max_replicas = Column(Integer, default=1)
    hpa = Column(String(400), default='')
    metrics = Column(Text(65536), default='')
    health = Column(String(400), default='')
    sidecar = Column(String(400), default='')
    ports = Column(String(100), default='80')
    resource_memory = Column(String(100), default='2G')
    resource_cpu = Column(String(100), default='2')
    resource_gpu = Column(String(100), default='0')
    deploy_time = Column(String(100), nullable=True, default=datetime.datetime.now)
    host = Column(String(200), default='')
    expand = Column(Text(65536), default='{}')
    canary = Column(String(400), default='')
    shadow = Column(String(400), default='')

    run_id = Column(String(100), nullable=True)
    run_time = Column(String(100))
    deploy_history = Column(Text(65536), default='')

    priority = Column(
        Integer, default=1
    )  # giving priority to meeting high-priority resource needs

    model_version_id = Column(Integer, ForeignKey('model_version.id'))
    model_version_info = relationship(Model_Version, foreign_keys=[model_version_id])
    image_id = Column(Integer, nullable=True)  # 镜像ID
    describe = Column(String(1000), nullable=True, default='')  # 服务描述
    service_id = Column(Integer, ForeignKey('service.id'), comment='服务ID')
    service = relationship(Service, foreign_keys=[service_id])
    specifications = Column(String(100), default='', comment='资源规格')
    weight = Column(Integer, nullable=True, comment='分流权重')
    invocation_times = Column(
        BigInteger, nullable=False, server_default='0', default=0, comment='调用次数'
    )  # 调用成功次数
    failed_times = Column(
        BigInteger, nullable=False, server_default='0', default=0, comment='调用失败次数'
    )
    gpu_type = Column(String(100), default='nvidia', server_default='nvidia', comment='GPU类型')
    region = Column(String(100), nullable=False, default='default', server_default='default', comment='地区')

    def gen_random_name(self, service_id=None):
        if service_id is None:
            service_id = self.service_id
        rand_str = uuid.uuid4().hex[0:8]
        rand_name = f'srv-ins-{service_id}-{rand_str}'
        self.name = rand_name
        return rand_name

    def get_cluster_host(self):
        ports = self.ports.split(',')
        default = '80'
        if len(ports) > 0:
            default = ports[0]

        return 'http://{}.service.svc.cluster.local:{}'.format(
            self.name.replace('_', '-'), default
        )

    def get_model_path(self):
        path = self.model_path

        return parse_model_path(path)

    def get_infer_code_path(self):
        path = self.model_path
        if path is None:
            return None
        path = path.strip()
        infer_code_path = None
        if path.startswith('{') and path.endswith('}'):
            path = json.loads(path)
            infer_code_path = path.get('infer_code_path', None)
        if infer_code_path is None:
            return None
        infer_code_path = infer_code_path.strip()

        return infer_code_path

    @property
    def replicas_html(self):
        return '%s~%s' % (self.min_replicas, self.max_replicas)

    @property
    def resource(self):
        if is_modelarts():
            return '1 * Ascend 910(32GB) | ARM: 24 核 90 GB'
        return 'cpu:%s,memory:%s,gpu:%s' % (
            self.resource_cpu,
            self.resource_memory,
            self.resource_gpu,
        )

    @property
    def debug(self):
        return Markup(
            f'<a target=_blank href="/inferenceservice_modelview/debug/{self.id}">调试</a>'
        )

    @property
    def test_deploy(self):
        return Markup(f'<a href="/inferenceservice_modelview/deploy/test/{self.id}">部署测试</a>')

    @property
    def deploy(self):
        return Markup(f'<a href="/inferenceservice_modelview/deploy/prod/{self.id}">部署生产</a>')

    @property
    def clear(self):
        return Markup(f'<a href="/inferenceservice_modelview/clear/{self.id}">清理</a>')

    def __repr__(self):
        return self.name

    @property
    def inference_host_url(self):
        url = self.get_host_url()
        link = url
        if self.service_type == 'tfserving':
            link += '/v1/models/' + self.model_name
        if self.service_type == 'torch-server':
            link += ':8080/models'
        debug_link = link.replace('http://', 'http://debug.').replace('https://', 'https://debug.')
        debug_url = url.replace('http://', 'http://debug.').replace('https://', 'https://debug.')
        test_link = link.replace('http://', 'http://test.').replace('https://', 'https://test.')
        test_url = url.replace('http://', 'http://test.').replace('https://', 'https://test.')
        hosts = f"""
        <a target=_blank href="{link}">{url}</a>
        <br><a target=_blank href="{debug_link}">{debug_url}</a>
        <br><a target=_blank href="{test_link}">{test_url}</a>
        """

        hosts = f'<a target=_blank href="{link}">{url}</a>'
        return Markup(hosts)

    def clone(self):
        return InferenceService(
            project_id=self.project_id,
            name=self.name + '-copy',
            label=self.label,
            service_type=self.service_type,
            model_name=self.model_name,
            model_version=self.model_version,
            model_path=self.model_path,
            model_type=self.model_type,
            model_input=self.model_input,
            model_output=self.model_output,
            model_status='offline',
            transformer=self.transformer,
            images=self.images,
            working_dir=self.working_dir,
            command=self.command,
            args=self.args,
            env=self.env,
            volume_mount=self.volume_mount,
            node_selector=self.node_selector,
            min_replicas=self.min_replicas,
            max_replicas=self.max_replicas,
            hpa=self.hpa,
            metrics=self.metrics,
            health=self.health,
            sidecar=self.sidecar,
            ports=self.ports,
            resource_memory=self.resource_memory,
            resource_cpu=self.resource_cpu,
            resource_gpu=self.resource_gpu,
            deploy_time='',
            host=self.host,
            expand='{}',
            canary='',
            shadow='',
            run_id='',
            run_time='',
            deploy_history='',
        )

    def get_curl_cmd(self, host):
        try:
            apis = json.loads(self.model_version_info.apis)

            curl_cmd = 'curl --location '

            method = apis['httpMethod']
            url = apis['url']
            if method.strip().lower() == 'get':
                curl_cmd += '--request GET '

            if url == '/':
                url = ''

            # infer_url =  f"{host}/api/v1/infer/{self.service_id}{url}"
            curl_cmd += f"'{host}/maas/v1/chat/completions'"

            model_code = get_model_code(self.model_version_info.base_model_code, self.model_version_info.source_id, self.service_id)

            headers = {'Authorization': 'Bearer $API_KEY'}

            body_type = apis.get('httpBodyType', 'json')
            if body_type == 'json':
                headers['Content-Type'] = 'application/json'

            for k, v in headers.items():
                curl_cmd += ' \\\n'
                curl_cmd += f"--header '{k}: {v}' "

            payload = {}

            def dfs(input_data, out):
                for item in input_data:
                    typ = item['valueType']
                    key = item['key']
                    if typ.lower() == 'object':
                        out[key] = {}
                        dfs(item.get('children', []), out[key])
                    else:
                        out[key] = item.get('exampleValue', None)

            if body_type == 'json':
                dfs(apis.get('body', []), payload)
                payload['model'] = model_code
                curl_cmd += f"--data '{json.dumps(payload, ensure_ascii=False)}'"
            elif body_type == 'form-data':
                for item in apis.get('body', []):
                    typ = item['valueType']
                    key = item['key']
                    curl_cmd += ' \\\n'
                    if typ.lower() == 'files':
                        demo_val = item.get('exampleValue', '/path/to/file')
                        curl_cmd += f'--form \'{key}=@"{demo_val}"\''
                    else:
                        demo_val = item.get('exampleValue', '')
                        demo_val = demo_val.replace('\n', '\\n')
                        curl_cmd += f'--form \'{key}="{demo_val}"\''

            return curl_cmd

        except Exception:
            return ''

    @property
    def total_invocation_times(self):
        success_times = 0
        with log_exception:
            success_times = int(self.invocation_times)

        failed_times = 0
        with log_exception:
            failed_times = int(self.failed_times)

        return failed_times + success_times

def get_model_code(base_model_code, source_id, service_id):
    model_code = ''
    if base_model_code:
        model_code = f'{base_model_code}:'
    if source_id:
        model_code += f'train-{source_id}:'
    model_code += f'instance-{service_id}'
    return model_code
